Skip to content

feat: SetNetworkConfig — forwarding / masquerade / isolation (decision 16)#7

Open
bodaay wants to merge 1 commit into
mainfrom
feat/set-network-config
Open

feat: SetNetworkConfig — forwarding / masquerade / isolation (decision 16)#7
bodaay wants to merge 1 commit into
mainfrom
feat/set-network-config

Conversation

@bodaay

@bodaay bodaay commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the correctness gap helm called out: the admin UI has let operators toggle forwarding / masquerade / isolation since helm M5, but no caller pushed the result to a node, so the rules never took effect. helm PR #30 wires the controller side (helm nodes push-policy); this PR is the buoy half.

What it does

  • internal/netpolicy — new package owning the on-node application of NetworkConfig (DESIGN §3, decision 16).
    • Policy.Validate mirrors helm's rejection of masquerade/isolation without forwarding (defense in depth).
    • NftApplier renders an nftables ruleset into its own pharos_buoy table, applied atomically via add table; delete table; table { … } (the idempotent-reset idiom — works whether the table existed before or not). The /proc/sys forwarding switches are written separately for v4 and v6.
    • Manager wraps the applier with an in-memory last-applied snapshot — an identical second call returns applied=false without re-running nft. A failed Apply does not poison the cache; the next call still runs.
  • SetNetworkConfig RPC — validates the three booleans, calls Manager.Apply, returns SetNetworkConfigResponse{applied}. InvalidArgument on missing config or invalid combinations; Internal on applier failure.
  • cli/run.go wires netpolicy.NewManager(NewNftApplier()) into control.Options.NetPolicy. The applier resolves nft and /proc/sys/... at call time, so production picks up system defaults and tests inject paths.

Design notes

  • Buoy chose nftables over iptables — atomic table replacement makes idempotency trivial and avoids the rule-numbered insertions that make iptables hard to drive deterministically. The wire contract is just the three booleans, so the choice is buoy-local.
  • No persistence. The last-applied snapshot lives in memory only. A buoy restart re-applies on the next call — correct, because nftables rules don't survive reboot either, so re-application is what we'd want anyway.
  • Rules are scoped to awg0; the interface name is DefaultWGInterface with an injection point if helm ever needs to drive a non-default interface.

Tests

`gofmt` / `vet` / `golangci-lint` clean; `go test -race ./...` green.

  • Policy.Validate truth table — every combination of the three booleans, plus the two invalid ones (masquerade/isolation without forwarding).
  • nft renderer per combination — forwarding-only emits no table body (kernel default is fine); masquerade and isolation rules each appear/disappear independently; both together render both.
  • NftApplier exercised end-to-end with a shell-stub nft and tempdir `/proc/sys` paths — captures the nft stdin and asserts the forwarding switches flip. No root needed.
  • Manager idempotency — applies on change, no-ops on identical replay, doesn't poison the cache on a failed Apply.
  • SetNetworkConfig over mTLS — applies → replays as no-op → re-applies on change → returns InvalidArgument for masquerade-without-forwarding and for missing config.
  • The pre-existing "unimplemented canary" probe in server_test.go moved from SetNetworkConfig to RestartService (still Unimplemented) so the invariant keeps being verified.

Next

Per your sequence: B3 XRay when you open the docs PR defining XRayConfig, then B6 packaging.

…n 16)

Close the correctness gap helm called out: the admin UI has been letting
operators toggle forwarding / masquerade / isolation since helm M5, but
no caller pushed the result down to a node, so the rules never took
effect. helm PR #30 wires the controller side (helm nodes push-policy);
this lands the buoy side.

- internal/netpolicy: new package owning the on-node application of
  NetworkConfig (DESIGN §3, decision 16). Policy.Validate repeats helm's
  rejection of masquerade/isolation without forwarding. NftApplier
  renders an nftables ruleset (its own 'pharos_buoy' table, atomically
  reset via 'add table; delete table; table {…}') and writes the
  /proc/sys ip_forward switches. Manager wraps the applier with an
  in-memory last-applied snapshot for idempotent replays — a second
  identical call returns applied=false without re-running nft.
- internal/control/service.go: SetNetworkConfig RPC — validates the
  three booleans, calls netpolicy.Manager.Apply, returns
  SetNetworkConfigResponse{Applied}. InvalidArgument on a missing or
  helm-prevalidated-rejectable Policy; Internal on applier failure.
- internal/cli/run.go: wires netpolicy.NewManager(NewNftApplier()) into
  control.Options.NetPolicy. The applier is system-resolved at call
  time (nft on PATH, /proc/sys defaults).
- Tests: Policy.Validate truth table; nft renderer per combination
  (forwarding-only emits no table body; masquerade and isolation rules
  appear/disappear individually); NftApplier exercised with a
  shell-stub nft + tempdir /proc paths (no root needed); Manager
  idempotency (applies on change, no-ops on replay, doesn't poison
  cache on failure); SetNetworkConfig over mTLS — applies → replays
  → applies on change → InvalidArgument for masquerade-without-forwarding.
  Existing 'unimplemented canary' in server_test moved from
  SetNetworkConfig to RestartService (still Unimplemented).
- BUILD.md: record the network-policy section and the nftables choice.

Signed-off-by: Khalefa <Khalefa@alahmad.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant